跳到主要内容

Go 常见的内存泄露场景

什么是内存泄露

内存泄露是指程序在运行过程中,已经不再使用的内存没有被及时释放,导致可用内存逐渐减少,最终可能导致程序崩溃或系统性能下降。Go 语言虽然有 GC (垃圾回收器),但不当的编程习惯仍然会导致内存泄露。

Goroutine 泄露

永不退出的 Goroutine

// 错误示例:Goroutine 永远不会退出
func goroutineLeakForever() {
go func() {
for {
// 死循环,没有退出条件
time.Sleep(time.Second)
fmt.Println("运行中...")
}
}()
}

// 正确示例:使用 context 控制退出
func goroutineWithContext() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保 goroutine 能够退出

go func() {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine 退出")
return
default:
time.Sleep(time.Second)
fmt.Println("运行中...")
}
}
}()
}

Channel 阻塞导致的 Goroutine 泄露

// 错误示例:发送者被阻塞
func channelLeakSender() {
ch := make(chan int) // 无缓冲 channel

go func() {
ch <- 42 // 永远阻塞,没有接收者
fmt.Println("发送完成") // 永远不会执行
}()

// 主函数退出,但 goroutine 仍然阻塞
}

// 错误示例:接收者被阻塞
func channelLeakReceiver() {
ch := make(chan int)

go func() {
data := <-ch // 永远阻塞,没有发送者
fmt.Println("接收到:", data)
}()
}

// 正确示例:使用带缓冲的 channel
func channelWithBuffer() {
ch := make(chan int, 1) // 带缓冲

go func() {
ch <- 42 // 不会阻塞
fmt.Println("发送完成")
}()

// 即使没有立即接收,goroutine 也能正常退出
}

// 正确示例:使用 select + timeout
func channelWithTimeout() {
ch := make(chan int)

go func() {
select {
case ch <- 42:
fmt.Println("发送成功")
case <-time.After(5 * time.Second):
fmt.Println("发送超时,退出")
return
}
}()
}

内存引用泄露

Slice 引用导致的内存泄露

// 错误示例:切片引用导致大内存无法释放
func sliceReferenceLeak() []byte {
// 创建一个大切片 (1GB)
largeSlice := make([]byte, 1024*1024*1024)

// 只返回前 10 个字节,但整个底层数组都不能被 GC
return largeSlice[:10] // 内存泄露!
}

// 正确示例:复制需要的数据
func sliceReferenceFix() []byte {
largeSlice := make([]byte, 1024*1024*1024)

// 复制需要的数据到新切片
result := make([]byte, 10)
copy(result, largeSlice[:10])

// largeSlice 可以被 GC 回收
return result
}

Map 中未清理的 Key

// 错误示例:Map 中的过期数据未清理
type Cache struct {
data map[string]*LargeObject
mu sync.RWMutex
}

func (c *Cache) Set(key string, obj *LargeObject) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = obj // 数据会一直积累
}

func (c *Cache) Get(key string) *LargeObject {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}

// 正确示例:定期清理过期数据
type CacheWithTTL struct {
data map[string]*cacheItem
mu sync.RWMutex
}

type cacheItem struct {
value *LargeObject
expiry time.Time
}

func (c *CacheWithTTL) Set(key string, obj *LargeObject, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = &cacheItem{
value: obj,
expiry: time.Now().Add(ttl),
}
}

func (c *CacheWithTTL) cleanup() {
c.mu.Lock()
defer c.mu.Unlock()

now := time.Now()
for key, item := range c.data {
if now.After(item.expiry) {
delete(c.data, key) // 清理过期数据
}
}
}

定时器相关泄露

未停止的 Timer 和 Ticker

// 错误示例:Timer 未停止
func timerLeak() {
for i := 0; i < 1000; i++ {
timer := time.NewTimer(time.Hour)
// timer 未停止,会一直占用内存
go func() {
<-timer.C
fmt.Println("定时器触发")
}()
}
}

// 正确示例:及时停止 Timer
func timerCorrect() {
var timers []*time.Timer

for i := 0; i < 1000; i++ {
timer := time.NewTimer(time.Hour)
timers = append(timers, timer)

go func(t *time.Timer) {
select {
case <-t.C:
fmt.Println("定时器触发")
case <-time.After(10 * time.Second):
fmt.Println("提前退出")
return
}
}(timer)
}

// 清理所有定时器
for _, timer := range timers {
timer.Stop()
}
}

// 错误示例:Ticker 未停止
func tickerLeak() {
ticker := time.NewTicker(time.Second)
// 忘记调用 ticker.Stop()

go func() {
for range ticker.C {
fmt.Println("Tick")
}
}()
}

// 正确示例:使用 defer 确保 Ticker 停止
func tickerCorrect() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop() // 确保停止

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

go func() {
for {
select {
case <-ticker.C:
fmt.Println("Tick")
case <-ctx.Done():
return
}
}
}()
}

资源未释放导致的泄露

文件句柄泄露

// 错误示例:文件未关闭
func fileHandleLeak() error {
for i := 0; i < 1000; i++ {
file, err := os.Open("test.txt")
if err != nil {
return err
}
// 忘记关闭文件,导致文件句柄泄露

data := make([]byte, 1024)
file.Read(data)
}
return nil
}

// 正确示例:使用 defer 确保文件关闭
func fileHandleCorrect() error {
for i := 0; i < 1000; i++ {
file, err := os.Open("test.txt")
if err != nil {
return err
}
defer file.Close() // 确保文件关闭

data := make([]byte, 1024)
file.Read(data)
}
return nil
}

// 更好的示例:立即 defer
func fileHandleBest() error {
for i := 0; i < 1000; i++ {
func() error {
file, err := os.Open("test.txt")
if err != nil {
return err
}
defer file.Close() // 循环结束立即关闭

data := make([]byte, 1024)
_, err = file.Read(data)
return err
}()
}
return nil
}

HTTP 连接泄露

// 错误示例:HTTP Response Body 未关闭
func httpConnectionLeak() error {
for i := 0; i < 100; i++ {
resp, err := http.Get("https://example.com")
if err != nil {
return err
}
// 忘记关闭 Body,导致连接泄露

data, _ := io.ReadAll(resp.Body)
fmt.Println(len(data))
}
return nil
}

// 正确示例:确保 Body 关闭
func httpConnectionCorrect() error {
client := &http.Client{
Timeout: 10 * time.Second,
}

for i := 0; i < 100; i++ {
resp, err := client.Get("https://example.com")
if err != nil {
return err
}
defer resp.Body.Close() // 确保关闭

data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Println(len(data))
}
return nil
}

闭包引用导致的泄露

// 错误示例:闭包引用大对象
func closureLeak() {
largeData := make([]byte, 1024*1024*100) // 100MB

// 闭包引用了整个 largeData
callback := func() {
fmt.Printf("数据长度: %d\n", len(largeData))
}

// 即使只需要长度信息,整个 largeData 都无法被 GC
registerCallback(callback)
}

// 正确示例:只引用需要的数据
func closureCorrect() {
largeData := make([]byte, 1024*1024*100) // 100MB

// 只保存需要的信息
dataLength := len(largeData)

callback := func() {
fmt.Printf("数据长度: %d\n", dataLength)
}

// largeData 可以被 GC 回收
registerCallback(callback)
}

内存泄露检测方法

使用 pprof 分析

import (
_ "net/http/pprof"
"net/http"
"log"
)

func main() {
// 启动 pprof 服务
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()

// 你的主程序
runMainProgram()
}
# 分析堆内存
go tool pprof http://localhost:6060/debug/pprof/heap

# 分析 goroutine
go tool pprof http://localhost:6060/debug/pprof/goroutine

# 生成内存分析报告
curl http://localhost:6060/debug/pprof/heap > heap.prof
go tool pprof heap.prof

内存使用监控

func monitorMemory() {
var m runtime.MemStats

ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()

for range ticker.C {
runtime.ReadMemStats(&m)

fmt.Printf("分配的内存: %d KB\n", m.Alloc/1024)
fmt.Printf("总分配的内存: %d KB\n", m.TotalAlloc/1024)
fmt.Printf("系统内存: %d KB\n", m.Sys/1024)
fmt.Printf("GC 次数: %d\n", m.NumGC)
fmt.Printf("Goroutine 数量: %d\n", runtime.NumGoroutine())
fmt.Println("---")
}
}

内存泄露预防策略

最佳实践

  1. 资源管理

    • 使用 defer 确保资源释放
    • 及时关闭文件、网络连接、数据库连接
    • 停止不再需要的定时器和 ticker
  2. Goroutine 管理

    • 使用 context 控制 goroutine 生命周期
    • 避免创建永不退出的 goroutine
    • 合理使用带缓冲的 channel
  3. 内存管理

    • 避免大对象的切片引用
    • 定期清理 map 中的过期数据
    • 谨慎使用闭包,避免意外引用
  4. 监控和分析

    • 集成 pprof 进行性能分析
    • 监控关键内存指标
    • 定期进行内存泄露检查